package com.atguigu.utils;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

/**
 * JDBC工具类 ，封装了对数据库的的连接、增删改查操作以及资源的关闭
 * MySQL5.7及之前的版本，连接字符串"jdbc:mysql://localhost:3306/test"即可。“jdbc:mysql://{地址}:{端口号}/{数据库名}”
 *并且旧版的调用的是Class.forName("com.mysql.jdbc.Driver");
 *而MySQL8.0新版，却需要在字符串中加上参数“?serverTimezone=UTC”,即："jdbc:mysql://localhost:3306/test?serverTimezone=UTC"。
 *调用的是Class.forName("com.mysql.jc.jdbc.Driver");

 * @author hjwStrart
 * @time 2022-03-05 17:24
 *
 * 代升级到2.0版本在2.0版本需要考虑事务，新增一个返回连接的方法，将部分方法私有化例如closeResource().
 */
public class DBUtils<Y> {

    public static Connection conn;
    private static PreparedStatement ps;
    private static ResultSet rs;

    {
//
        Type genericSuperclass = this.getClass().getGenericSuperclass();
        ParameterizedType pt = (ParameterizedType) genericSuperclass;
        Type[] aTG = pt.getActualTypeArguments();
        Class<? extends Type> aClass = aTG[0].getClass();
    }
    static{


 //        为不影响程序的可移植性，因尽量避免在程序中出现第三方类库,使用配置文件减少第三方jar包的出现

        //使用配置文件获取MySQL厂商驱动
        Properties properties = new Properties();
        try {
            //使用类加载器读取获取输入流，可以避免引入其它类
            InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("db.properties");
            properties.load(is);
            String classDriver = properties.getProperty("db.mysql.Driver");
            String username = properties.getProperty("db.username");
            String password = properties.getProperty("db.password");
            String url = properties.getProperty("db.url8");
            Class.forName(classDriver);
            conn = DriverManager.getConnection(url, username, password);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 根据输入的sql语句，将结果集中的第一条记录封装为指定类型对象，并返回。
     * 该方法会自动关闭statement和ResultSet对象
     * @param clazz 返回的队对象类型
     * @param sql   需要执行的查询语句
     * @param args  占位符参数
     * @param <T>   泛型，返回数据类型
     * @return t     查询对象
     * @throws Exception
     */
    public static <T> T queryForOne(Class<T> clazz, String sql, Object... args) {

        T t = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            ps = conn.prepareStatement(sql);

//                注入占位符
            if (!Objects.isNull(args)) {
                for (int i = 0; i < args.length; i++) {

                    //数据库中的第一个字段索引位置从1开始
                    ps.setObject(i + 1, args[i]);
                }
            }

//        获取结果集
            rs = ps.executeQuery();

//        判断结果集是否为空
            if (rs.next()) {
                //     实例化t,必须确保T具有默认构造器
                t = clazz.newInstance();
                //        通过ResultSetMeatData获取查询结果集列数
                ResultSetMetaData md = rs.getMetaData();
                //     获取列数
                int columnCount = md.getColumnCount();
                for (int i = 0; i < columnCount; i++) {
                    // 获取结果集的列名
                    //  在java程序中，和数据库交互的方法，一旦涉及到索引一般是从一开始
                    //  在源码中的的注释：columnIndex – the first column is 1, the second is 2, ...
//                    此处也有要修改，getColumnName获得的是列名，不是别名
//                    使用getColumnLabel优先获得别名，不存在别名，获得列名
//                    String columnName = md.getColumnName(i+1);

                    //                优先获取字段别名，没有别名获取字段名
                    String columnName = md.getColumnLabel(i + 1);
                    Object value = rs.getObject(i + 1);
                    //            使用反射动态设值
                    //获取field对象
                    Field field = clazz.getDeclaredField(columnName);
                    field.setAccessible(true);
//                     在此处可能出现java.lang.NoSuchFieldException: 字段名，原因是因为在T中没有指定字段，通过使用别名可以解决该问题，当然也可修改T的字段名
                    field.set(t, value);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeResource(ps, rs);
        }
        return t;

    }

    /**
     * 根据输入的sql语句，将结果集中的记录封装为指定类型的对象集合，并返回。
     * 该方法会自动关闭statement和ResultSet对象
     * @param clazz     返回的队对象类型
     * @param sql   需要执行的查询语句
     * @param args  占位符参数
     * @param <T>  泛型，返回数据类型
     * @return    结果集对象集合
     */
    public static <T> List<T> queryForList(Class<T> clazz, String sql, Object... args) {

        List<T> list = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        T t;
        try {
            ps = conn.prepareStatement(sql);

//                注入占位符
            if (!Objects.isNull(args)) {
                for (int i = 0; i < args.length; i++) {

                    //数据库中的第一个字段索引位置从1开始
                    ps.setObject(i + 1, args[i]);
                }
            }

//        获取结果集
            rs = ps.executeQuery();

//        判断结果集是否为空
            if (rs.next()) {
                //            实例化list
                list = new ArrayList<>();
                //        通过ResultSetMeatData获取查询结果集列数
                ResultSetMetaData md = rs.getMetaData();
                //     获取列数
                int columnCount = md.getColumnCount();
                String columnName;
                Object value;
                do  {

                    t = clazz.newInstance();

                    for (int i = 0; i < columnCount; i++) {

                        //                优先获取字段别名，没有别名获取字段名
                         columnName = md.getColumnLabel(i + 1);
                         value = rs.getObject(i + 1);
                        //            使用反射动态设值
                        //获取field对象
                        Field field = clazz.getDeclaredField(columnName);
                        field.setAccessible(true);
                        field.set(t, value);
                    }
                    list.add(t);
                }while (rs.next());//这里使用do-while循环，避免少一行记录。每调用一次rs的next方法，游标都会下移一行
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeResource(ps, rs);
        }
        return list;

    }

    /**
     * 执行增删改操作，返回受影响的行数
     * @param sql       需要执行的查询语句
     * @param args      占位符参数
     * @return          影响的行数
     */
    public static int update(String sql,Object ... args){

        int result = 0;
        try {
            result = 0;
             ps = conn.prepareStatement(sql);
//             注入占位符
            if (args != null) {
                for (int i = 0; i < args.length; i++) {
                    ps.setObject(i+1,args[i]);
                }
            }
            result = ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        return result;


    }

    /**
     * 释放数据库连接资源
     *
     */
    public static void closeResource() {

        if (!Objects.isNull(conn)) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        closeResource(ps,rs);

    }



    public static void closeResource(Statement st, ResultSet rs) {
        if (!Objects.isNull(st)) {
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (!Objects.isNull(rs)) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
